{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "91c95bbf-6ba0-4a17-813e-f6b46b16785e",
   "metadata": {},
   "source": [
    "# Hardware Initialization and Configuration\n",
    "\n",
    "This section initializes the FPGA hardware by loading the overlay bitstream and connecting to the matrix multiplication accelerator IP. The code configures the PL (Programmable Logic) clock and displays the available IP cores and register map of the accelerator.\n",
    "\n",
    "Key components:\n",
    "- Loads the FPGA bitstream for the systolic array matrix multiplier\n",
    "- Displays the current PL clock frequency (100MHz)\n",
    "- Enumerates available IP blocks in the design\n",
    "- Maps the register interface of the matrix multiplication accelerator for control and data exchanges"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "f16e386b-d561-4ddd-b549-302086be0937",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/javascript": [
       "\n",
       "try {\n",
       "require(['notebook/js/codecell'], function(codecell) {\n",
       "  codecell.CodeCell.options_default.highlight_modes[\n",
       "      'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n",
       "  Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n",
       "      Jupyter.notebook.get_cells().map(function(cell){\n",
       "          if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n",
       "  });\n",
       "});\n",
       "} catch (e) {};\n"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/javascript": [
       "\n",
       "try {\n",
       "require(['notebook/js/codecell'], function(codecell) {\n",
       "  codecell.CodeCell.options_default.highlight_modes[\n",
       "      'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n",
       "  Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n",
       "      Jupyter.notebook.get_cells().map(function(cell){\n",
       "          if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n",
       "  });\n",
       "});\n",
       "} catch (e) {};\n"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "current IP PL clock hz: 99.999Mhz\n",
      "dict_keys(['mmult_accel_0', 'zynq_ultra_ps_e_0'])\n",
      "RegisterMap {\n",
      "  CTRL = Register(AP_START=0, AP_DONE=0, AP_IDLE=1, AP_READY=0, RESERVED_1=0, AUTO_RESTART=0, RESERVED_2=0, INTERRUPT=0, RESERVED_3=0),\n",
      "  GIER = Register(Enable=0, RESERVED=0),\n",
      "  IP_IER = Register(CHAN0_INT_EN=0, CHAN1_INT_EN=0, RESERVED_0=0),\n",
      "  IP_ISR = Register(CHAN0_INT_ST=0, CHAN1_INT_ST=0, RESERVED_0=0),\n",
      "  A_1 = Register(A=write-only),\n",
      "  A_2 = Register(A=write-only),\n",
      "  B_1 = Register(B=write-only),\n",
      "  B_2 = Register(B=write-only),\n",
      "  C_1 = Register(C=write-only),\n",
      "  C_2 = Register(C=write-only),\n",
      "  N = Register(N=write-only),\n",
      "  K = Register(K=write-only),\n",
      "  M = Register(M=write-only),\n",
      "  update_A = Register(update_A=write-only)\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "from pynq import Overlay\n",
    "from pynq import Clocks\n",
    "\n",
    "overlay = Overlay(\"/home/ubuntu/workspace/pynq_bitfiles/2-28/MatMul_SA10.bit\")\n",
    "overlay.download()\n",
    "print(f\"current IP PL clock hz: {Clocks.fclk0_mhz}Mhz\")\n",
    "\n",
    "print(overlay.ip_dict.keys())\n",
    "accel_ip = overlay.mmult_accel_0\n",
    "print(accel_ip.register_map)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f2c02280-dc6a-4b11-84d5-ecbe4636623a",
   "metadata": {},
   "source": [
    "# Core Functions and Utilities\n",
    "\n",
    "This section implements the core functionality required for FPGA-CPU interaction and performance measurement. It defines functions for matrix multiplication execution and energy measurement.\n",
    "\n",
    "Key components:\n",
    "- `call_fpga()`: Handles memory management and parameter configuration for the hardware accelerator\n",
    "- `read_power()`: Reads system power consumption from the hardware power monitor\n",
    "- `measure_energy()`: Measures energy consumption during function execution\n",
    "- Implements proper memory synchronization between CPU and FPGA using flush/invalidate operations\n",
    "- Supports persistent weight storage optimization via the `update_A` parameter"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "fa1e4f99-aa6b-4685-8b6d-caca9f0688c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import torch\n",
    "from pynq import allocate\n",
    "\n",
    "def call_fpga(A_buf, B_buf, C_buf, accel_ip, N, K, M, update_A):\n",
    "    \"\"\"\n",
    "    Runs a 2D matrix multiplication on the FPGA accelerator:\n",
    "      (N, K) x (K, M) => (N, M)\n",
    "\n",
    "    A_buf, B_buf, C_buf are PYNQ buffers allocated with shape=(N,K), (K,M), (N,M).\n",
    "    update_A: 1 to load A into BRAM (new input), 0 to reuse persistent A.\n",
    "    \"\"\"\n",
    "    print(\"calling fpga, update_A =\", update_A)\n",
    "    \n",
    "    # Flush input buffers to ensure data consistency.\n",
    "    # Only flush A_buf if we intend to update A (update_A==1).\n",
    "    if update_A:\n",
    "        A_buf.flush()\n",
    "    B_buf.flush()\n",
    "\n",
    "    # Configure the accelerator registers\n",
    "    accel_ip.register_map.A_1 = A_buf.physical_address & 0xFFFFFFFF\n",
    "    accel_ip.register_map.A_2 = (A_buf.physical_address >> 32) & 0xFFFFFFFF\n",
    "    accel_ip.register_map.B_1 = B_buf.physical_address & 0xFFFFFFFF\n",
    "    accel_ip.register_map.B_2 = (B_buf.physical_address >> 32) & 0xFFFFFFFF\n",
    "    accel_ip.register_map.C_1 = C_buf.physical_address & 0xFFFFFFFF\n",
    "    accel_ip.register_map.C_2 = (C_buf.physical_address >> 32) & 0xFFFFFFFF\n",
    "    accel_ip.register_map.N = N\n",
    "    accel_ip.register_map.K = K\n",
    "    accel_ip.register_map.M = M\n",
    "    # Pass the update_A flag to the accelerator\n",
    "    accel_ip.register_map.update_A = update_A\n",
    "\n",
    "    # Start the accelerator\n",
    "    accel_ip.register_map.CTRL.AP_START = 1\n",
    "\n",
    "    # Wait for finish\n",
    "    while accel_ip.register_map.CTRL.AP_DONE == 0:\n",
    "        pass\n",
    "\n",
    "    # Invalidate output buffer so the CPU sees the updated data from DDR\n",
    "    C_buf.invalidate()\n",
    "    \n",
    "def read_power():\n",
    "    \"\"\"Reads power from hwmon2 (returns in Watts)\"\"\"\n",
    "    try:\n",
    "        with open(\"/sys/class/hwmon/hwmon2/power1_input\", \"r\") as f:\n",
    "            power_microwatts = int(f.read().strip())  # Read power in µW\n",
    "            return power_microwatts / 1e6  # Convert to Watts\n",
    "    except FileNotFoundError:\n",
    "        print(\"Power sensor not found\")\n",
    "        return None\n",
    "    \n",
    "def measure_energy(func, *args):\n",
    "    \"\"\"Measures total energy used by `func` in Joules\"\"\"\n",
    "    power_samples = []\n",
    "    timestamps = []\n",
    "\n",
    "    start_time = time.time()\n",
    "\n",
    "    # Run the function while sampling power\n",
    "    result = func(*args)  # Execute the computation\n",
    "\n",
    "    active_power = read_power()  # Measure power after running workload\n",
    "    elapsed_time = time.time() - start_time  # Compute execution time\n",
    "\n",
    "    avg_power = active_power  # Since we measure before & after execution\n",
    "    energy_used = avg_power * elapsed_time  # E = P × t\n",
    "\n",
    "    return result, energy_used, avg_power  # Subtract idle power to isolate workload power"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8af7e722-6be4-4d6b-a725-0afc25c4dec8",
   "metadata": {
    "tags": []
   },
   "source": [
    "# Memory Allocation and Initialization\n",
    "\n",
    "This section prepares the data structures needed for matrix multiplication benchmarking. It allocates physically contiguous memory buffers accessible by both the CPU and FPGA accelerator.\n",
    "\n",
    "Key components:\n",
    "- Defines matrix dimensions for the benchmark (N=64, K=768, M=3072)\n",
    "- Allocates non-cacheable memory buffers for input matrices A and B and output matrix C\n",
    "- Initializes input matrices with random int8 values to simulate quantized neural network operations\n",
    "- Ensures data consistency by flushing CPU caches to main memory"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "a47aa02a-fcad-4ca3-8782-0eeae5320560",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 2) Prepare Buffers (Using a small example for demonstration)\n",
    "# Suppose we want to multiply a [N, K] x [K, M] => [N, M]\n",
    "# DistilBERT typical Q/K/V matmul: shape [batch*seq_len, 768] x [768, 768]\n",
    "# For demonstration, let’s do a small random size. We’ll adapt logic for DistilBERT below.\n",
    "N, K, M = 64, 768, 3072\n",
    "A_buf = allocate(shape=(N, K), dtype=np.int8, cacheable=False)\n",
    "B_buf = allocate(shape=(K, M), dtype=np.int8, cacheable=False)\n",
    "C_buf = allocate(shape=(N, M), dtype=np.int32, cacheable=False) # accelerator writes int32 results\n",
    "\n",
    "# 3) Initialize Input Data (simulate random int8 values)\n",
    "A_buf[:] = np.random.randint(-128, 127, size=(N,K), dtype=np.int8)\n",
    "B_buf[:] = np.random.randint(-128, 127, size=(K,M), dtype=np.int8)\n",
    "# A_buf[:] = np.zeros((N, K), dtype=np.int8)\n",
    "# B_buf[:] = np.zeros((K, M), dtype=np.int8)\n",
    "\n",
    "# flush caches so data is in DDR\n",
    "A_buf.flush()\n",
    "B_buf.flush()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cb6fc545-e0a9-42e8-a8ef-d75795c2fe4d",
   "metadata": {},
   "source": [
    "# FPGA Accelerator Benchmark\n",
    "\n",
    "This section executes and measures the performance of the FPGA-based matrix multiplication accelerator. It captures detailed timing and power metrics for the hardware implementation.\n",
    "\n",
    "Key components:\n",
    "- Separates data transfer time from computation time for accurate performance analysis\n",
    "- Measures system power during accelerator execution\n",
    "- Records energy consumption for the complete hardware pipeline\n",
    "- Implements proper memory transfer protocol (flush before compute, invalidate after compute)\n",
    "- Demonstrates the use of the `measure_energy()` function to capture power efficiency metrics"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "21de160c-2d11-43ef-b155-a6381ce4ff14",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "calling fpga, update_A = 1\n",
      "FPGA Energy: 0.334752 J, Average system power: 3.490 W\n"
     ]
    }
   ],
   "source": [
    "import time\n",
    "\n",
    "# ============================================\n",
    "# 🚀 Step 3: BENCHMARK - FPGA ACCELERATOR\n",
    "# ============================================\n",
    "\n",
    "# FPGA Execution\n",
    "start_data_in = time.time()\n",
    "A_buf.flush()\n",
    "B_buf.flush()\n",
    "end_data_in = time.time()\n",
    "\n",
    "start_power_fpga = read_power()\n",
    "start_fpga = time.time()\n",
    "_,fpga_energy, fpga_power = measure_energy(lambda: call_fpga(A_buf, B_buf, C_buf, accel_ip, N, K, M, update_A=1))\n",
    "end_fpga = time.time()\n",
    "end_power_fpga = read_power()\n",
    "\n",
    "start_data_out = time.time()\n",
    "C_buf.invalidate()\n",
    "result_fpga = C_buf[:, :].copy()\n",
    "end_data_out = time.time()\n",
    "\n",
    "print(f\"FPGA Energy: {fpga_energy:.6f} J, Average system power: {fpga_power:.3f} W\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5daf1797-8df1-43e5-bdd8-b4074341b033",
   "metadata": {},
   "source": [
    "# CPU Reference Implementations\n",
    "\n",
    "This section benchmarks CPU-based matrix multiplication using NumPy and PyTorch libraries as reference implementations. These measurements establish the baseline for evaluating FPGA acceleration benefits.\n",
    "\n",
    "Key components:\n",
    "- Implements NumPy integer matrix multiplication as a reference benchmark\n",
    "- Implements PyTorch tensor-based matrix multiplication as a modern optimized baseline\n",
    "- Measures execution time and energy consumption for both CPU implementations\n",
    "- Enables direct comparison between FPGA and CPU approaches under identical workloads"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "2e0e17bd-e084-4f5e-b8bf-24641805a085",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "NumPy CPU Energy: 70.453704 J, Average system power: 3.400 W\n",
      "PyTorch CPU Energy: 2.234749 J, Average system power: 3.460 W\n"
     ]
    }
   ],
   "source": [
    "# ===========================\n",
    "# 💻 Step 4: CPU REFERENCE (NumPy & PyTorch)\n",
    "# ===========================\n",
    "start_cpu_numpy = time.time()\n",
    "ref_numpy, numpy_energy, numpy_power = measure_energy(lambda: np.matmul(A_buf.astype(np.int32), B_buf.astype(np.int32)))\n",
    "# ref_numpy = -1\n",
    "end_cpu_numpy = time.time()\n",
    "\n",
    "start_cpu_torch = time.time()\n",
    "device = torch.device(\"cpu\")\n",
    "ref_torch, torch_energy, torch_power = measure_energy(\n",
    "    lambda: torch.matmul(torch.tensor(A_buf.astype(np.int32)), torch.tensor(B_buf.astype(np.int32)))\n",
    ")\n",
    "ref_torch_np = ref_torch.cpu().numpy()\n",
    "end_cpu_torch = time.time()\n",
    "\n",
    "print(f\"NumPy CPU Energy: {numpy_energy:.6f} J, Average system power: {numpy_power:.3f} W\")\n",
    "print(f\"PyTorch CPU Energy: {torch_energy:.6f} J, Average system power: {torch_power:.3f} W\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d9d49bcb-5d72-48aa-a623-285d48ce43e8",
   "metadata": {},
   "source": [
    "# Result Verification and Accuracy Analysis\n",
    "\n",
    "This section verifies the correctness of the FPGA implementation by comparing its output against CPU reference implementations. It also calculates key performance metrics for comparison.\n",
    "\n",
    "Key components:\n",
    "- Validates accelerator results against NumPy and PyTorch reference implementations\n",
    "- Computes maximum error between hardware and software outputs\n",
    "- Calculates total operations (multiply-accumulate) performed in the matrix multiplication\n",
    "- Computes throughput metrics (operations per second) for all implementations\n",
    "- Prepares speedup metrics comparing the FPGA against CPU implementations"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "6ea710d3-7b1e-49f1-a223-e9b98c671c0c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[ 222745 -198604 -125826 ... -230950 -252629  111507]\n",
      " [-185581 -148148  -82416 ...  -44409 -414185  315982]\n",
      " [ 280127  287019  111515 ...  -59646   -3909 -231869]\n",
      " ...\n",
      " [-245183  -33391  -51268 ... -227903   24360   88982]\n",
      " [  59980  120954 -330594 ... -121153  286411  107239]\n",
      " [-155963  100746  146587 ...  333351 -210819  244740]]\n",
      "[[ 222745 -198604 -125826 ... -230950 -252629  111507]\n",
      " [-185581 -148148  -82416 ...  -44409 -414185  315982]\n",
      " [ 280127  287019  111515 ...  -59646   -3909 -231869]\n",
      " ...\n",
      " [-245183  -33391  -51268 ... -227903   24360   88982]\n",
      " [  59980  120954 -330594 ... -121153  286411  107239]\n",
      " [-155963  100746  146587 ...  333351 -210819  244740]]\n"
     ]
    }
   ],
   "source": [
    "# ============================\n",
    "# 🧪 Step 5: ACCURACY CHECK\n",
    "# ============================\n",
    "diff_numpy = np.abs(ref_numpy - result_fpga)\n",
    "max_err_numpy = np.max(diff_numpy)\n",
    "\n",
    "diff_torch = np.abs(ref_torch_np - result_fpga)\n",
    "max_err_torch = np.max(diff_torch)\n",
    "\n",
    "# ============================\n",
    "# 📊 Step 6: PERFORMANCE METRICS\n",
    "# ============================\n",
    "total_ops = 2 * N * K * M\n",
    "\n",
    "acc_latency = end_fpga - start_fpga\n",
    "total_hw_time = end_data_out - start_data_in\n",
    "hw_throughput = (total_ops / acc_latency) / 1e9\n",
    "hw_end_to_end = (total_ops / total_hw_time) / 1e9\n",
    "\n",
    "sw_time_numpy = end_cpu_numpy - start_cpu_numpy\n",
    "sw_throughput_numpy = (total_ops / sw_time_numpy) / 1e9\n",
    "speedup_latency_numpy = sw_time_numpy / acc_latency\n",
    "speedup_total_numpy = sw_time_numpy / total_hw_time\n",
    "\n",
    "sw_time_torch = end_cpu_torch - start_cpu_torch\n",
    "sw_throughput_torch = (total_ops / sw_time_torch) / 1e9\n",
    "speedup_latency_torch = sw_time_torch / acc_latency\n",
    "speedup_total_torch = sw_time_torch / total_hw_time\n",
    "\n",
    "print(result_fpga)\n",
    "print(ref_torch_np)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "80699d1b-b175-4efc-8123-17870fcfac85",
   "metadata": {},
   "source": [
    "# Performance Visualization and Analysis\n",
    "\n",
    "This section presents comprehensive performance metrics and creates visualizations comparing the different implementations. The results highlight the advantages of the FPGA-based accelerator.\n",
    "\n",
    "Key components:\n",
    "- Displays formatted performance summary with emoji indicators for readability\n",
    "- Shows latency, throughput, and energy consumption metrics for all implementations\n",
    "- Calculates and displays speedup factors between FPGA and CPU implementations\n",
    "- Creates publication-quality bar charts comparing:\n",
    "  - Execution time (lower is better)\n",
    "  - Computational throughput (higher is better)\n",
    "  - Energy consumption (lower is better)\n",
    "- Uses logarithmic scale to properly visualize wide-ranging performance differences"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "1c8f89af-31a4-4c8d-b40a-5b299786325f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "\n",
       "## **🎯 Performance Comparison: FPGA vs NumPy vs PyTorch**\n",
       "\n",
       "### **Matrix Multiplication Details**\n",
       "- **Matrix Shape**: `[64, 768] × [768, 3072]`\n",
       "- **Accuracy Check**:\n",
       "  - ✅ **Max Difference (NumPy vs FPGA)**: `0`\n",
       "  - ✅ **Max Difference (PyTorch vs FPGA)**: `0`\n",
       "\n",
       "---\n",
       "\n",
       "### **📊 Latency & Throughput**\n",
       "| Framework  | Latency (sec) | Throughput (GFLOPs) |\n",
       "|------------|--------------|----------------------|\n",
       "| 🧮 NumPy   | `20.722516`  | ⚡ `0.01` GFLOPs |\n",
       "| 🔥 PyTorch | `0.678447`  | ⚡ `0.45` GFLOPs |\n",
       "| 🚀 FPGA    | `0.096725`  | ⚡ `3.12` GFLOPs |\n",
       "\n",
       "- **⏱️ Total HW Execution Time**: `0.106130` sec  \n",
       "- **⚡ Overall FPGA Throughput**: `2.85` GFLOPs  \n",
       "\n",
       "---\n",
       "\n",
       "### **🚀 Speedup Comparison**\n",
       "| Comparison  | Speedup (Latency) | Speedup (Total) |\n",
       "|-------------|------------------|-----------------|\n",
       "| FPGA vs NumPy   | `214.24×` 🚀🚀🚀 | `195.26×` 🚀🚀🚀 |\n",
       "| FPGA vs PyTorch | `7.01×` 🚀 | `6.39×` 🚀 |\n",
       "\n",
       "✅ **Test Completed!** 🎯\n"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjgAAALICAYAAABy54rvAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAALEwAACxMBAJqcGAAAVJZJREFUeJzt3WmYHVW5t/H7IQlJCDOIIhCCTMoYMDKpiC0qoyCgTCKggtAHBGlUjkdfUfEIB1pQIApoBBRRRBECKKgtIKCMhklUwtQkzIEwhDHJ836o6rDT9LC7053dqdy/69oXu6pWrVpVXaH/vWpVVWQmkiRJVbJEoxsgSZI00Aw4kiSpcgw4kiSpcgw4kiSpcgw4kiSpcgw4kiSpcgw4ariIeGtEXBcRL0REa6Pbo55FxP4RcXWj26HuRcRDEbF9o9sBEBEZEes0uh1a/Bhw1C/l/0BfjogXI+KJiDg3IpbuZ3WHAk8Dy2ZmywA2c0iLiP0i4tbyGD4WEb+PiPc1ul29ycwLMvMjjWxDf3+BR8Q1EfG5wWjTgoiId0fEbeW58J+I+GgPZfcvy71Y/hucWzP94sJsdyNFxLgyPA1vdFs0NBlwtCB2zcylgc2BCcDX+rJyFJYA1gT+mf146uSi+j+3iDgGOA34X+CtwFhgIrBbA5vVq0X1eC8CzgB+DywDfBSY1l3BMmAuXf7b2xF4tGO6nNcn/kxVVQYcLbDMnE7xP+eNACJiq4i4MSJmRsQdEbFdR9nyL+jvRMQNwEvA+cCBwJfLv0C3j4iREXFaRDxafk6LiJHl+ttFxLSI+EpEPA78NCKOj4hfR8TPy8tcd0XEehHx3xHxZEQ8EhEfqWnDwRFxb1n2gYj4fM2yjvpbynUfi4iDa5aPjojWiHg4Ip6LiOsjYnRv+10rIpYDvgX8V2b+NjNnZebrmTk5M79UlqnnGHy5po27R8RO5V//z0TEV2u2d3xEXBwRvyr3+faI2LRm+XERcX+57J8R8fGaZQdFxA0RcWpEzACOL+ddXy6PctmTEfF8eew7zoPlIuL8iHiqPF5fKwNtR73XR8QpEfFsRDwYETvWe851JyJWiIjLy20+W35fvVz2HeD9wBnluXZGOf+dEfHH8rj9OyI+WVPfuRFxZkRcUR6fmyJi7ZrlG9as+0REfDUi3hYRL0XESjXlNi/bNKKbpr8OPJyFBzPzngU9FsD4iLizPE9/FRGjyrZ09W+op/Nt3s+7Zn/mXXaKiJUiYnL5878lIk7oXB7YPiLuK/9tnBkRUVP3DRFxRtnOf0XEh2q2M19PXXku/7ycvK7878zy57n1ABwzVUlm+vHT5w/wELB9+X0N4B7g28BqwAxgJ4oA/eFy+i1l2WuAdmBDYDgwAjgXOKGm7m8BfwdWAd4C3Ah8u1y2HTAbOAkYCYwGjgdeofjLdzhFaHoQ+J+y/kOAB2vq3xlYGwjgAxRBa/NO9X+rXHencvkK5fIzy31YDRgGbFO2o8f97nTsdii3MbyH41vPMfh/Nfv3FPALih6ADYGXgbXK8sdT/ALdqyx/bHl8RpTLPwG8vWz33sAsYNVy2UHlto4sj+3oct715fKPArcBy5fH8101654PXFq2aRzwH+CzNfW+XrZ9GHA48CgQ5fLjgMvrOf86zV8J2BNYqtzur4Hf1Sy/BvhczfQY4BHg4HL/NqO4XLpBufzc8ue4Rbn8AuCX5bJlgMeAFmBUOb1luexK4PCa7ZwKnN7D/rQCz1Keh334d7gdMK2b43Nz+XNdEbgXOKyHf0M9nW/zft419SewTvn9l+VnKWCD8nhe36ns5eU5MpbiXN2h0/n1RYpzc2/gOWDFrn7OFOfyz8vv48q6u/135Gfx/jS8AX4WzU/5P54XgZnAwxSXV0YDXwF+1qnsVcCB5fdrgG91Wn4u8wec+4GdaqY/CjxUft8OeA0YVbP8eOCPNdO7lm0bVk4vU/6PcPlu9uV3wFE19b9c+z9N4ElgK4oA8DKwaRd19LjfnebvDzzey/Ht7Ri83MX+bVlT/jZg95rj8/eaZUtQ/GJ+fzfbngLsVn4/CGjvtPwg3gg4TRTBZStgiZoyw8qf0wY18z4PXFNTx9SaZUuV+/C2Ppx/bwo4XZQbDzxbM30N8wecvYG/dlrnLOAbNefmj2uW7QT8q/y+L/CPbra7N3BDzbF4HNiim7L7ALdTXG6azhthe3vgtl72bzu6Dzifqpn+P+BHPfwb6ul8m/fzrlmewDrlvr0OrF+z7ATeHHDeVzN9EXBcTd3zgm0572bggK5+zhhw/PTh4yUqLYjdM3P5zFwzM5sz82WK8TSfKLuiZ0bETOB9wKo16z3SS71vpwhNHR4u53V4KjNf6bTOEzXfXwaezsw5NdMASwNExI4R8ffyssJMil9aK9esPyMzZ9dMv1SuuzLFX+r3d9HmevZ7Xv3AytHz2IfejsGMLvav8zGoHY8x75hn5lyKMR5vB4iIT0fElJp2b8T8x6Pbn1dmtlGMHzkTeDIizo6IZcv1R3SxD6vVTD9eU89L5df+DlSn3JelIuKs8pLY8xSXMZaPiGHdrLImsGWnn9v+wNu6aidvnAtQ9Fx2dS5A0XO1QUSsRdGb91xm3txN2aOAkzPz9xQh8PcRsTnwXqCtp/3tRXfthjf/G+rtfOvOWyh6tmrPka7Ol57aMj0za8ff1bttqUcGHA20Ryh6Mpav+YzJzBNryvQ2mPhRil88HcaW8+pdv1vluILfAKcAb83M5SkuJ0Qdqz9NcSls7S6W1bPfHf4GvArs3sO2ejsGfbVGx5dyHMzqwKMRsSZwDnAEsFJ5PO5m/uPR4/HOzB9k5rspLk+sB3yJ4li93sU+TF+AfahHC7A+RW/WssC25fyO/em8L48A13b6uS2dmYfXsa1HgHd0taAMDxcBnwIOAH7WQz0dl2rJzMuBY4Crgc9QhMfB0Pk49HS+zaLoYQMgImrD31MUl5hWr5m3Bn2zWseYnN62zfzBs9//H9DiwYCjgfZzYNeI+GhEDIuIUeWgxtV7XfMNFwJfi4i3RMTKFGNNft7LOvVakmLcwVPA7HJga123PJc9H5OA70XE28v927oMTXXvd2Y+V+7TmVEMDl4qIkaUPUv/VxYb6GPw7ojYo+w1OpoiYP2dYgxKlseDKAZUb1RvpRHxnojYshw8O4siAM4te5cuAr4TEcuUQeqYBdyHzkaUx7njM5zict3LFANPVwS+0WmdJ5g/lFwOrBcRB5Q/gxHlPr2rju1fDqwaEUeXg3SXiYgta5afT3EJ5mP0HHB+Dfy/iNi0DJ//oejlGF1HGwZKT+fbHcCGETG+HKh8fMdK5c/5txSDz5eKiHcCn+7jtlcBvlAe+09QjOO6slw2BdinXDaBYhxZh6eAuXQTMiUDjgZUZj5CcavzVyn+B/QIxV/0fTnXTgBuBe4E7qIYn3DCALXvBeALFL98nwX2Ay7rQxXHlm26BXiGYqDmEn3d78xspfiF/7Wa8kdQjAeCgT8Gl1KMC3mWokdhjyzu3PonxQDXv1H88t8YuKEP9S5L0QP0LMWlhRnAyeWyIylCzwPA9RSDoCfVU2kUdyP9vpdiV1KEmY7P8RS33o+m6EH6O/CHTut8H9grijusflCeDx+hGAfzKMWllI7Btz0q1/0wxZivx4H7gA/WLL+B4hfw7Zn5cJeVFE6hOC6XAC8AZ1P0RJ0HXBHFXXeDrdvzLTP/QzEI+U8U+9j5DqkjgOUojsHPKMLSq33Y9k3AuhQ/s+8Ae2XmjHLZ1yl6TJ8FvklxDlG266Wy/A3l5cWt+rBNLQY67liQVFERcTzFHS+fanRbFjcR0Qb8IjN/3Oi2LCwRcRLFYPED6yh7EMWg7yH/gEsteuzBkaRBEBHvoXgI5q8a3ZbBFMVzhDaJwhbAZyl6o6SG8gmWkjTAIuI8ikHkR5WXsqpsGYrLUm+nuMzZSnFJVGooL1FJkqTK8RKVJEmqnEpeolpiiSVy9OiFeYelJEnV99JLL2VmLhKdI5UMOKNHj2bWrFmNboYkSZUSES/3XmpoWCRSmCRJUl8YcCRJUuUYcCRJUuUYcCRJUuUYcCRJUuUYcCRJUuUYcCRJUuUYcCRJUuUYcCRJUuUYcCRJUuUYcCRJUuUYcCRJUuVU8mWbg+GrE59sdBMWuv9tXqXRTZAkqV8MOJKkartk10a3YOH7+ORGt6DhKhVwImJXYNeRI0c2uimSJKmBKjUGJzMnZ+ahw4YNa3RTJElSA1Uq4EiSJIEBR5IkVZABR5IkVY4BR5IkVY4BR5IkVY4BR5IkLbCIGBURN0fEHRFxT0R8s4syIyPiVxExNSJuiohxg9UeA44kSRoIrwJNmbkpMB7YISK26lTms8CzmbkOcCpw0mA1xoAjSZIWWBZeLCdHlJ/sVGw34Lzy+8XAhyIiBqM9BhxJklSv4RFxa83n0NqFETEsIqYATwJ/zMybOq2/GvAIQGbOBp4DVhqUhg5GpZIkqZJmZ+aE7hZm5hxgfEQsD1wSERtl5t0LrXU17MGRJEkDKjNnAn8Bdui0aDqwBkBEDAeWA2YMRhsMOJIkaYFFxFvKnhsiYjTwYeBfnYpdBhxYft8LaMvMzuN0BoSXqCRJ0kBYFTgvIoZRdKBclJmXR8S3gFsz8zLgJ8DPImIq8Aywz2A1xoAjSZIWWGbeCWzWxfz/V/P9FeATC6M9XqKSJEmVY8CRJEmVY8CRJEmVY8CRJEmVs1gNMn799deZNm0ar7zySp/X3WXCnEFo0dB2772D8miCRcqoUaNYffXVGTFiRKObIknqg8Uq4EybNo1lllmGcePG0ddXX0x78vVBatXQtfoqi/cv9cxkxowZTJs2jbXWWqvRzZEk9cFidYnqlVdeYaWVVupzuNHiKSJYaaWV+tXjJ0lqrMUq4ACGG/WJ54skLZoWu4AjSZKqb7Eag9PZVyc+WXfZV17r/VUZx+y3Yq9l1l9rBf794LN1bfNvN1zLiCWXZMJ7tq6r/EA77bTTWHHFFfn0pz89IPXts88+fPvb32bdddcdkPokSeqOPThD2N9uvJZbb/lbQ7Y9e/ZsJk2axH777TdgdR5++OH83//934DVJ0lSdww4Q8Afr7qcXXd4Lzt86D3su9cOPPXkEzzS/hA/P+8cfnzWD/ho0wRu+vv1zHj6KQ79zCfZ+aNbs/NHt+aWm28E4Hsnf4uWow7hEx/fnve+Z30mnXPGvLovvuhnfHi7zfnIB9/NUf91EC+++ALbTFiP118v7gp74YXn55vucMP1f2HzzTdn+PCik+8HP/gBG2ywAZtssgn77FO8G23WrFl85jOfYYsttmCzzTbj0ksvBWDOnDkce+yxbLTRRmyyySacfvrpALz//e/nT3/6E7Nnzx7cAypJWuwt1peohor3bPleLvv99UQEF/58Ej88s5X/983/41MHHsJSY5bmsOZjADjisAP43OePYost38v0ae18ap+d+cv1dwFw/9R/86vf/pFZL77AB967EQcc9HkeuP8//ODU7/K7y69jxZVW5tlnn2HppZdhq/duy5//eCU77LQbl11yETvsvPubnvNy68038u53v3ve9IknnsiDDz7IyJEjmTlzJgDf+c53aGpqYtKkScycOZMtttiC7bffnvPPP5+HHnqIKVOmMHz4cJ555hkAllhiCdZZZx3uuOOO+eqWJGmgGXCGgMcenUbzofvz5BOP8frrr7HG2K6fuXL9dW3c9597502/8MILzJr1IgBN2+/IyJEjGTlyJCuv/BaefuoJbrz+GnbedU9WXGllAFZYoRgjtO/+n+GHZ5zCDjvtxkW/PI+TWn/4pm09+cTjbDlho3nTm2yyCfvvvz+77747u+++OwBXX301l112GaeccgpQ3Ibf3t7On/70Jw477LB5vT8rrvjG2KRVVlmFRx991IAjSRpUBpwh4P/9zxc55PNH8ZEdduVvN1zL9075dpfl5uZcLr3yekaNGvWmZUuOHDnv+xJLDOvxMtB7ttiG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Pzxv2a67fQKAB+7/N2uMHcc71l4PgL0+eQA3/f2vPdZ7y0038LHdPwkU75dabvkV6tpmxzuqtt12W55//nlmzpzZr/16+umnWX755btd/vGPf5yNNtqIPfbYY9682ktUC/pwwP3335/x48dzww03cMopp/COd7yDBx54gCOPPJI//OEPLLvssvPKrrLKKjz66KMLtD1J0tBgwBkiap++vMSwYcyZ88Z4m6WW6v21H8OGDWfu3GIg9KuvvrLA24yI+cpGBMOHv7ENgFde6X07o0ePnq/chhtuyO233z5v+pJLLuHcc8+d743j9dpggw247bbb5pt32223seGGG86bvuCCC5gyZQq/+93vWGONNVhhhRW444472G677fjRj37E5z73ufn2Z/To0X1uhyRp6DHgLELesfb6THvkYR58cCoAv7n4ArbaelsA1lhjTe66swgOV15+ybx1JmyxDZdfdjEA117zR56b+Wxd25p86a8BuP7661luueVYbrnlGDdu3Lxwcvvtt/Pggw8CsMwyy/DCCy90Wc8KK6zAnDlz5oWc/fbbjxtuuIHLLnvjCQAvvfRSfQegky9/+ct85StfYcaMGQBMmTKFc889t9u7uaDoUZo7dy577rknJ5xwwnxh6z//+Q8bbbRRt+tKkhYdQ/5Jxn0REbsCu44cObK+FfrwttUZQ+Bt4qNGjaL1tHM4/HP7Mnv2bDYdP4FPHXgoAEcf+zW+9MVDOfmk49l6mw/MW+eLLV/jiMMO4De/voB3T9iKVVZ5G2OWXoZZs17scVsjR45is8024/XXX2fSpEkA7Lnnnpx//vlsuOGGbLnllqy3XnGpbKWVVuK9730vG220ETvuuOObxuF85CMf4frrr2f77bdn9OjRXH755RxzzDEcffTRvPWtb2WZZZbha1/7Wq/7//nPf56jjz4agDXWWIO//e1vTJ8+nW222YaIYJllluHnP/85q666ard1TJ8+nYMPPnheT9R3v/tdAF5//XWmTp3KhAkTem2HJGnoi8xsdBsG3JgxY3LWrFlvmn/vvffyrne9q191ThsCAac/Xn31VYYNG8bw4cO57Za/89WvHMFVbbf2uM4nPr49X/vGSez8ka0GpA233347p556Kj/72c8GpL7BcMkll3D77bfz7W9/+03LFuS8kTQE9OGZZ5XRhz/g+yIiXsrM3sdNDAGV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\n",
      "text/plain": [
       "<Figure size 576x720 with 3 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import display, Markdown\n",
    "\n",
    "# Define speedup emoji function\n",
    "def speedup_emoji(speedup):\n",
    "    if speedup >= 25:\n",
    "        return \"🚀🚀🚀\"\n",
    "    elif speedup >= 10:\n",
    "        return \"🚀🚀\"\n",
    "    elif speedup >= 1:\n",
    "        return \"🚀\"\n",
    "    return \"🐢\"\n",
    "\n",
    "# Generate Markdown dynamically\n",
    "md_text = f\"\"\"\n",
    "## **🎯 Performance Comparison: FPGA vs NumPy vs PyTorch**\n",
    "\n",
    "### **Matrix Multiplication Details**\n",
    "- **Matrix Shape**: `[{N}, {K}] × [{K}, {M}]`\n",
    "- **Accuracy Check**:\n",
    "  - {\"✅\" if max_err_numpy == 0 else \"❌\"} **Max Difference (NumPy vs FPGA)**: `{max_err_numpy}`\n",
    "  - {\"✅\" if max_err_torch == 0 else \"❌\"} **Max Difference (PyTorch vs FPGA)**: `{max_err_torch}`\n",
    "\n",
    "---\n",
    "\n",
    "### **📊 Latency & Throughput**\n",
    "| Framework  | Latency (sec) | Throughput (GFLOPs) |\n",
    "|------------|--------------|----------------------|\n",
    "| 🧮 NumPy   | `{sw_time_numpy:.6f}`  | ⚡ `{sw_throughput_numpy:.2f}` GFLOPs |\n",
    "| 🔥 PyTorch | `{sw_time_torch:.6f}`  | ⚡ `{sw_throughput_torch:.2f}` GFLOPs |\n",
    "| 🚀 FPGA    | `{acc_latency:.6f}`  | ⚡ `{hw_throughput:.2f}` GFLOPs |\n",
    "\n",
    "- **⏱️ Total HW Execution Time**: `{total_hw_time:.6f}` sec  \n",
    "- **⚡ Overall FPGA Throughput**: `{hw_end_to_end:.2f}` GFLOPs  \n",
    "\n",
    "---\n",
    "\n",
    "### **🚀 Speedup Comparison**\n",
    "| Comparison  | Speedup (Latency) | Speedup (Total) |\n",
    "|-------------|------------------|-----------------|\n",
    "| FPGA vs NumPy   | `{speedup_latency_numpy:.2f}×` {speedup_emoji(speedup_latency_numpy)} | `{speedup_total_numpy:.2f}×` {speedup_emoji(speedup_total_numpy)} |\n",
    "| FPGA vs PyTorch | `{speedup_latency_torch:.2f}×` {speedup_emoji(speedup_latency_torch)} | `{speedup_total_torch:.2f}×` {speedup_emoji(speedup_total_torch)} |\n",
    "\n",
    "✅ **Test Completed!** 🎯\n",
    "\"\"\"\n",
    "\n",
    "# Display the Markdown\n",
    "display(Markdown(md_text))\n",
    "\n",
    "# ================================\n",
    "# 📊 Visualization\n",
    "# ================================\n",
    "\n",
    "# Data for plotting\n",
    "frameworks = [\"NumPy\", \"PyTorch\", \"FPGA\"]\n",
    "latencies = [sw_time_numpy, sw_time_torch, acc_latency]  # Lower is better\n",
    "throughputs = [sw_throughput_numpy, sw_throughput_torch, hw_throughput]  # Higher is better\n",
    "energies = [numpy_energy, torch_energy, fpga_energy]  # Energy in Joules\n",
    "\n",
    "x = np.arange(len(frameworks))  # X-axis positions\n",
    "\n",
    "# Create figure\n",
    "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 10))\n",
    "\n",
    "# Bar width\n",
    "bar_width = 0.4\n",
    "\n",
    "# Plot latency & throughput (first subplot)\n",
    "ax1.bar(x - bar_width / 2, latencies, width=bar_width, label=\"Latency (sec)\", color='royalblue', alpha=0.7)\n",
    "ax1.set_ylabel(\"Latency (seconds)\", color='royalblue')\n",
    "ax1.set_yscale(\"log\")  # Log scale for better visualization\n",
    "\n",
    "ax3 = ax1.twinx()\n",
    "ax3.bar(x + bar_width / 2, throughputs, width=bar_width, label=\"Throughput (GFLOPs)\", color='darkorange', alpha=0.7)\n",
    "ax3.set_ylabel(\"Throughput (GFLOPs)\", color='darkorange')\n",
    "\n",
    "ax1.set_xticks(x)\n",
    "ax1.set_xticklabels(frameworks)\n",
    "ax1.set_title(\"Performance Comparison: Latency & Throughput\")\n",
    "fig.legend(loc=\"upper left\", bbox_to_anchor=(0.1, 0.92))\n",
    "\n",
    "# Plot energy consumption (second subplot)\n",
    "ax2.barh(x, energies, color='forestgreen', alpha=0.7)  # Thinner bars\n",
    "ax2.set_xlabel(\"Energy (Joules)\")\n",
    "ax2.set_yticks(x)\n",
    "ax2.set_yticklabels(frameworks)\n",
    "ax2.set_title(\"Energy Consumption Comparison\")\n",
    "ax2.set_xscale(\"log\")  # Apply log scale to energy axis\n",
    "\n",
    "# Adjust layout and show plot\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
